梦入琼楼寒有月,行过石树冻无烟

Node.js 多线程

进程\线程\协程

进程

在此之前,我们需要了解到进程、协程、以及并发、并行的概念。首先进程(process) 是指计算机中已运行的程序,进程的出现是指为了更好的利用 CPU 资源使得并发的可能性。

假设有两个任务,A 和 B ,A 遇到 I/O 操作的时候 CPU 就会默默的等待任务 A 读取完成再去执行 B 任务,这样救护导致 CPU 资源造成了浪费。

因此就会有人在想,将任务 A 读取数据时让 B 任务执行,当任务 B 读取完整数据后又切换到任务 A 执行。

这时候因为 “切换” 的问题,需要涉及到保存和状态回复,以及 A 与 B 的系统资源不一,就需要有一个东西记录 A 和 B 分别需要什么样的资源和如何识别 A 和 B 等,因此进程就被创建出来

通过进程可以分配资系统资源、标识任务,以及分配 CPU 执行进程为调度,进程的状态记录、恢复。其中切换又被称之为 “上下文切换”,其中进程是指系统分配的最小单位,进程所占用的资源共有地址空间,全局变量、文件描述符、各种硬件资源等。

线程

线程(tread) 是操作系统能够进行运算调度的最小单位,大部分情况下他被包含在进程中,他的出现主要是为了降低上下文的消耗以提高系统的并发性,并解决一个进程只能完成一件任务的问题,使得进程内并发成为可能。

在之前进程处理任务的时候,当只有一个进程的时候只能干一件事,而假设有了多个进程,每个进程只能负责一个任务,A 负责接收一个信息,B 负责显示信息,C 负责保存信息,这里的各个进程之间就会涉及到进程通信的问题,假设共同维护一个文本内容或切换,则会造成性能上的消耗。

此时线程主要的作用就是使得 A、B、C 共享资源,并参与 CPU 的调度,同时进程也是线程的容器,当一个现成挂掉则全部线程也会失效。

协程

协程(coroutine) 通过线程中实现调度以避免陷入内核级别的上下文切换所造成的性能损失,进而突破了线程在 I/O 上的性能瓶颈。

当涉及到较大的并发连接时,以线程作为处理单元(系统调度开销开始过大),当连接数过多时则需要大量的线程,可能部分的线程处于 ready(准备) 状态,系统会不断的进行上下文切换。

而瓶颈就是上下文切换,在线程中自己实现调度从而不陷入内核级的上下文切换,这也是协程的主要作用。

并发与并行

并发

并发计算(Concurrent computing) 是一种程序的计算形式,通常是指有两个以上工作单位的计算在同时运行,这也是多核的前提,但单线程或单核是很难利用多进程达到并行状态。

并行

并行(parallel computing) 一般是指许多工作任务同时进行的计算模式,可以将计算的过程分为许多小部分,之后以并发来解决。

总结来说并发是指可以同时出发,而并行则是指一起执行,你可以理解为一边一吃饭一边玩游戏是并发。而你将外卖摆在桌上和游戏,你先吃饭再打游戏是并发。

JavaScript 单线程的异步和非阻塞

线程安全

单线程的 JavaScript 保证了线程的绝对安全,不会单行同一个变量被多个线程读写从而造成崩溃,从而免去了在多线程开发中忘记对变量加锁或者解锁所造成的问题。

单线程的异步非阻塞

Node.js 是一个单进程的,因此提供了一个 libuv 库来进行实现多进程,如在 fs 库中的多进程就通过 libuv 库进行实现,他主要提供了异步 I/O 。

虽然 node.js 是单线程异步非阻塞的,但在某一个情况下他采用的还是阻塞式的单线程

libuv


libuv 是一个跨平台的 I/O 库,用于实现和让 node.js 支持多线程,同时也被 fs 使用他主要提供事件循环(Event Loops)、文件系统(Filesystem)、网络支持(Networking)、线程(Threads)、进程(Process)以及其他工具(Utilities)

Web Worker 实现多进程

实现多进程可以通过 Node API 中的 child_process ,所提供的 fork 方法来创建一个 Node.js 文件并将它作为 worker 文件,类似与 express 库:

app.js

.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express')
const fork = require('child_process').fork
const app = express()

app.get('/', function (req,res) {
var worker = fork('./work.js') // 创建进程
worker.on('message', function (m) { // 接收计算结果
if ('object' === typeof m && m.type == 'fibo') {
// 发送关闭进程信号
worker.kill()
res.send(m.result.toString()) // 返回结果
}
})
worker.send({type:'fibo', num:~~req.query.n || 1})
})
app.listen(8210)

其中 worker.kill 的用法是通过 work.js 中的 process.on 监听 SIGHUP 事件函数并通过 process.exit 从而使子进程推出的

work.js

.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义算法
var fibo = function fibo(n) {
return n > 1 ? fibo(n -1) + fibo(n -2) :1
}

// 接受 app.js 主进程所发送的消息
process.on('message', function (m) {
if (typeof m === 'object' && m.type() === 'fibo') {
// 计算 fibo
var num = fibo(~~m.num)

// 发送结果
process.send({type:'fibo', result:num})
}
})

// 收到消息后进程退出
process.on('SIGHUP',function () {
process.exit()
})
⬅️ Go back